CI/CD with Jenkins and Ansible

Balázs Németh
ITNEXT
Published in
7 min readJul 3, 2018

--

It’s 2018. Kubernetes won the container orchestration battle. Some of us are jealously reading those articles by the Silicon Valley startups (yeah maybe they are already in your city, too!) but then we go back to our good old legacy systems.

Trunk-based development, containers in the cloud are on the road map but in short term they are simply not possible to implement.

A step into the DevOps direction is to eliminate silos (dev, QA, ops) and therefore we have to structure our code in a way that it enables each role to collaborate easily.

I’ve read a lot of very good articles about how to build a Spring Boot backend with some single page Javascript app, also about configuration management, infrastructure provisioning, continuous integration and delivery but now I’m going to combine all that, and provide some scaffolding for you to build on.

Setup

What I have at hand is a Jenkins instance, ssh and a nice runnable Spring Boot jar. Also a RedHat7 VM and a Nexus as artifact repository. So I guess I should be happy I’m not deploying EARs anymore!

Jenkins with Ansible — a simple but powerful combination

Now I’m going to build a deployment pipeline with those tools and put everything into version control, so that everyone on the team has access to everything and knows what happens with their piece of code from commit to deployment (in this case only until a test environment).

I use the following structure:

parent
+- backend
+- frontend
+- deployment
Jenkinsfile

For the sake of simplicity backend — a Spring Boot app — contains the frontend ReactJS app, deployment is where the tools are for continuous delivery, and the Jenkinsfile in the root directory is the declarative descriptor of our pipeline.

Spring Boot with React — a common choice for web applications

Let’s have a little look into those modules!

Backend

First it inherits from the Spring Boot parent:

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
<relativePath/>
</parent>

Let’s include the frontend app among other dependencies:

<dependencies>
...
<dependency>
<groupId>com.company.skeleton</groupId>
<artifactId>frontend</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
...
</dependencies>

I also use Spotbugs , Checkstyle and Jacoco for static analysis and code coverage, so we have to include those plugins as well. Notice the security plugin of Spotbugs which is a small shift left on security.

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
</configuration>
</plugin>
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>3.1.3.1</version>
<configuration>
<effort>Max</effort>
<threshold>Low</threshold>
<failOnError>true</failOnError>
<plugins>
<plugin>
<groupId>com.h3xstream.findsecbugs</groupId>
<artifactId>findsecbugs-plugin</artifactId>
<version>LATEST</version>
</plugin>
</plugins>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.0.0</version>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.1</version>
<configuration>
<fileSets>
<fileSet>
<directory>${project.build.directory}</directory>
<includes>
<include>*.exec</include>
</includes>
</fileSet>
</fileSets>
</configuration>
<executions>
<execution>
<id>default-prepare-agent</id>
<phase>process-classes</phase>
<goals>
<goal>prepare-agent</goal>
</goals>
<configuration>
<destFile>${project.build.directory}/jacoco.exec</destFile>
</configuration>
</execution>

<execution>
<id>pre-integration-test</id>
<phase>pre-integration-test</phase>
<goals>
<goal>prepare-agent</goal>
</goals>
<configuration>
<destFile>${project.build.directory}/jacoco-it.exec</destFile>
<propertyName>failsafeArgLine</propertyName>
</configuration>
</execution>
<execution>
<id>post-integration-test</id>
<phase>post-integration-test</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<dataFile>${project.build.directory}/jacoco-it.exec</dataFile>
<outputDirectory>${project.reporting.outputDirectory}/jacoco-it</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
</plugin>
</plugins>
</build>

Now we’re ready to move to frontend.

Frontend

As we need a library that we can include as a maven dependency, we’ll copy the built resources into the public directory of that jar with the maven-resources-plugin.

But first we need to build and test this module as well. We’ll use the frontend-maven-plugin for that but both of these steps could be done with a script or directly within the Jenkinsfile if one doesn’t like the maven approach.

<build>
<plugins>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
<executions>
<execution>
<id>prepare-package</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${basedir}/target/classes/public</outputDirectory>
<resources>
<resource>
<directory>${project.basedir}/build</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.6</version>
<executions>
<execution>
<id>install node and yarn</id>
<goals>
<goal>install-node-and-yarn</goal>
</goals>
<configuration>
<nodeVersion>v9.9.0</nodeVersion>
<yarnVersion>v1.5.1</yarnVersion>
</configuration>
</execution>
<execution>
<id>yarn</id>
<goals>
<goal>yarn</goal>
</goals>
<phase>prepare-package</phase>
<configuration>
</configuration>
</execution>
<execution>
<id>yarn build</id>
<goals>
<goal>yarn</goal>
</goals>
<phase>prepare-package</phase>
<configuration>
<arguments>build</arguments>
</configuration>
</execution>
<execution>
<id>test</id>
<goals>
<goal>yarn</goal>
</goals>
<phase>test</phase>
<configuration>>
<arguments>test</arguments>
<environmentVariables>
<CI>true</CI>
</environmentVariables>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

Now let’s build everything with Jenkins!

Jenkinsfile

We are going to create the following pipeline here:

And we’re using the declarative way.

In the Build stage we build frontend and backendin parallel.

Of course we have to keep in mind that backend depends on the artifact produced by the frontend module, therefore we have to include another step to create the runnable jar after the two parallel builds but this time we can skip running the tests.

pipeline {
agent { label 'RHEL' }
tools {
maven 'Maven 3.3.9'
jdk 'jdk1.8.0'
}
stages {
stage('Build') {
parallel {
stage('Build Backend'){
steps {
dir('backend'){
sh 'mvn clean test spotbugs:spotbugs checkstyle:checkstyle'
}
}
post {
always {
junit 'backend/target/surefire-reports/*.xml'
findbugs canComputeNew: false, defaultEncoding: '', excludePattern: '', healthy: '', includePattern: '', pattern: '**/spotbugsXml.xml', unHealthy: ''
checkstyle canComputeNew: false, defaultEncoding: '', healthy: '', pattern: '**/checkstyle-result.xml', unHealthy: ''
jacoco()
}
}
}
stage('Build Frontend'){
steps {
dir('frontend'){
sh 'mvn clean install'
}
}
}
}
}
stage('Create runnable jar'){
steps {
dir('backend'){
sh 'mvn deploy -DskipTests'
}
}
}
}
}

Probably you noticed I used mvn deploy and not mvn install , and that’s because we’re using an artifact repository here which is Nexus.

That’s our single source of truth where all our built artifacts are stored. This is the place where we will pull the artifacts from in all of our environments.

This artifact repository has to be defined in the backend ‘s pom.xml .

<distributionManagement>
<repository>
<uniqueVersion>true</uniqueVersion>
<id>Releases</id>
<layout>default</layout>
<url>http://nexus.edudoo.com/</url>
</repository>
<snapshotRepository>
<uniqueVersion>false</uniqueVersion>
<id>Snapshots</id>
<layout>default</layout>
<url>http://nexus.edudoo.com/</url>
</snapshotRepository>
</distributionManagement>

Deployment

As I mentioned I have a RedHat7 virtual machine and ssh access. The simplest tool that requires ssh access only is Ansible, so we go with that, and it has to be installed on the Jenkins node.

for simple IT automation

Another decision to make is how to run our application. We could create some shell scripts to start/stop the java jar but a little bit more sophisticated solution is to use a process/service manager.

I could have chosen Supervisor or others but that’s not out of the box supported on our RedHat Linux machine, so let’s just stick with systemd.

The steps that we are going to perform each time are as follows:

  • prepate the environment by installing the required packages,
  • prepare and push the configuration of the application,
  • pull the jar from Nexus,
  • create (or update) and (re)start the systemd service.

In our case creating the environment means that the packages are updated and java is installed. These are defined in the role common :

- name: Ensure kernel is at the latest version
yum: name=kernel state=latest

- name: Install latest Java 8
yum: name=java-1.8.0-openjdk.x86_64 state=latest

The deploy role contains the rest, first we pull the jar into a directory in /opt:

- name: Create skeleton directory
file: path=/opt/skeleton state=directory

- name: Download skeleton runnable jar
get_url:
url:
http://nexus.edudoo.com/artifact/maven/content?g=com.edudoo.skeleton&a=backend&v=0.0.1-SNAPSHOT&r=snapshots
dest: /opt/skeleton/skeleton.jar
backup: yes
force: yes

Now the configuration management part:

- name: Ensure app is configured
template:
src:
application.properties.j2
dest: /opt/skeleton/application.properties

- name: Ensure logging is configured
template:
src:
logback-spring.xml.j2
dest: /opt/skeleton/logback-spring.xml

The Spring boot app is configured by the application.properties file placed next to the runnable jar. With the template above we can replace its content from environment to environment.

Let’s have a look at the template itself:

server.port={{skeleton_port}}
logging.config=/opt/skeleton/logback-spring.xml
logging.file=/opt/skeleton/skeleton.log

When we’re running our ansible script, skeleton_port will be replaced by a provided value. We’re coming back to this later.

(The same applies for the log configuration.)

Finally the part about the service:

- name: Install skeleton systemd unit file
template: src=skeleton.service.j2 dest=/etc/systemd/system/skeleton.service

- name: Start skeleton
systemd: state=restarted name=skeleton daemon_reload=yes

The template actually doesn’t contain any variables at the moment (but could, eg. java args to dynamically control the memory consumption):

[Unit]
Description=Skeleton Service

[Service]
User=root
WorkingDirectory=/opt/skeleton/
ExecStart=/usr/bin/java -Xmx256m -jar skeleton.jar
SuccessExitStatus=143
TimeoutStopSec=10
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

What’s left to define is an inventory file (eg. dev-servers) containing the environments:

[test]
11.22.33.44
[prod]
11.22.33.45
11.22.33.46

and a playbook ( site.yml) which holds together all the steps:

---
- hosts: test
remote_user: clouduser
roles:
- common
- deploy

vars:
- skeleton_port: 80

Note that we define a value for the variable skeleton_porthere which will be replaced in the template of the application.properties file.

So let’s add that to our Jenkinsfile :

...
stage('Deploy to test'){
steps {
dir('deployment'){ //do this in the deployment directory!
echo 'Deploying to test'
sh 'ansible-playbook -i dev-servers site.yml'
}
}
}
...

Now we are ready, we only need to commit everything into a git repository, and let Jenkins know that the Jenkinsfile can be pulled from there.

Configuring Jenkins

In Jenkins you should create a new Multibranch Pipeline and on the configuration page the only thing to set is the source:

Jenkins configuration

Save, run and enjoy!

The code is available at https://github.com/balazsmaria/skeleton

--

--